package org.rcgwt.generators;

import java.beans.PropertyDescriptor;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.beanutils.PropertyUtils;
import org.rcgwt.client.model.BeanProperty;
import org.rcgwt.client.model.IsReflected;
import org.rcgwt.client.model.Reflected;
import org.rcgwt.client.model.ReflectedFactory;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JPackage;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

/**
 * Generator of {@link Reflected} wrapper from {@link IsReflected} bean<br/>
 * 
 * 
 * @author Didier MOISE
 */
public class ReflectedGenerator extends Generator {

	private static final String FACTORY_PACKAGE = "org.rcgwt.client.model";

	private static final String FACTORY_NAME = "ReflectedFactoryImpl";

	private TypeOracle typeOracle;

	/** GWT logger */
	private TreeLogger logger;

	private GeneratorContext context;

	/**
	 * Generate the reflection factory implementation.
	 */
	@Override
	public String generate(TreeLogger logger, GeneratorContext context,
			String typeName) throws UnableToCompleteException {

		typeOracle = context.getTypeOracle();
		assert (typeOracle != null);

		this.logger = logger;
		this.context = context;

		/** List of reflected beans */
		List<JClassType> reflectedBeans = listReflectedBeans(typeOracle);

		// Class composer
		ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(
				FACTORY_PACKAGE, FACTORY_NAME);
		composer.setSuperclass(ReflectedFactory.class.getCanonicalName());
		composer.addImport(ReflectedFactory.class.getName());
		composer.addImport(BeanProperty.class.getName());

		// Factory PrintWriter
		PrintWriter pw = context.tryCreate(logger, FACTORY_PACKAGE,
				FACTORY_NAME);

		// Generate the factory
		if (pw != null) {
			// Source writer
			SourceWriter sw = composer.createSourceWriter(context, pw);

			sw.println("public <T> Reflected<T> createReflected(T obj) {");
			sw.println("  String objClassName=obj.getClass().getName();");
			for (JClassType reflectedType : reflectedBeans) {
				// Create the reflected class
				String reflectedClassName = generateReflectedWrapper(reflectedType);

				// Add it to the "createReflected" method
				String beanClassFullName = reflectedType.getPackage().getName()
						+ "." + reflectedType.getName();
				sw.println("if (objClassName.equals(\"" + beanClassFullName
						+ "\"))  {");
				sw.println("	return (Reflected<T>) new " + reflectedClassName
						+ "((" + beanClassFullName + ")obj);");
				sw.println("}");
			}

			sw.println("return null;");
			sw.println("}");

			sw
					.println("public BeanProperty[] getBeanProperties(String objClassName) {");
			for (JClassType reflectedType : reflectedBeans) {
				// Create the reflected class
				generateBeanProperties(reflectedType, sw, reflectedType
						.getPackage().getName()
						+ "." + reflectedType.getName());
			}

			sw.println("return null;");

			sw.println("}");

			sw.commit(logger);

		}

		return composer.getCreatedClassName();
	}

	private List<JClassType> listReflectedBeans(TypeOracle typeOracle) {

		Set<JClassType> reflectedBeans = new HashSet<JClassType>();

		Set<JPackage> packages = new HashSet<JPackage>(Arrays.asList(typeOracle
				.getPackages()));
		filterPackages(packages);

		for (JPackage jp : packages) {
			findReflectedAnnotations(reflectedBeans, jp);
		}

		return new ArrayList<JClassType>(reflectedBeans);
	}

	private void findReflectedAnnotations(Set<JClassType> reflectedBeans,
			JPackage jp) {

		for (JClassType cl : jp.getTypes()) {
			org.rcgwt.client.model.IsReflected annotation = cl
					.getAnnotation(org.rcgwt.client.model.IsReflected.class);
			if (annotation == null) {
				continue;
			}

			reflectedBeans.add(cl);
		}

	}

	private void filterPackages(Set<JPackage> packages) {
		for (Iterator<JPackage> it = packages.iterator(); it.hasNext();) {
			String name = it.next().getName();

			if (name.startsWith("org.rcgwt.client")) {
				it.remove();
				continue;
			}

			if (name.startsWith("java")) {
				it.remove();
				continue;
			}

			if (name.startsWith("sun")) {
				it.remove();
				continue;
			}

			if (name.startsWith("com.google")) {
				it.remove();
				continue;
			}

		}
	}

	/**
	 * Generate the Reflected wrapper class.
	 */
	public String generateReflectedWrapper(JClassType reflectedType)
			throws UnableToCompleteException {

		// Generate the name and the package of the reflected class
		String beanClassFullName = reflectedType.getPackage().getName() + "."
				+ reflectedType.getName();
		String reflectedPackage = reflectedType.getPackage().getName();
		String reflectedName = "Reflected_" + reflectedType.getName();

		// Initialize the source writer
		SourceWriter src = initReflectedClass(reflectedType, reflectedName,
				reflectedPackage, beanClassFullName);
		if (src != null) {

			src.println("public " + reflectedName + "("
					+ reflectedType.getName() + " bean) {");
			src.println("  super(bean);");
			src.println("}");

			// Generate the source code of the reflected class
			generateGettersAndSettersCallers(reflectedType, src,
					beanClassFullName);

			// Finalize the source writer
			finalizeReflectedClass(src);
		}

		// Return the reflected class full name
		return reflectedPackage + "." + reflectedName;
	}

	/**
	 * Generate the source code of the Reflected Wrapper
	 * 
	 * @param bean
	 * @param src
	 */
	private void generateBeanProperties(JClassType bean, SourceWriter sw,
			String beanClassName) throws UnableToCompleteException {

		// Find the bean class
		Class<?> beanClass;
		String jBeanClass = bean.getPackage().getName() + "." + bean.getName();
		try {
			beanClass = Class.forName(jBeanClass);
		} catch (Exception e) {
			logger.log(TreeLogger.ERROR, "Unable to find class  '" + jBeanClass
					+ "'", null);
			throw new UnableToCompleteException();
		}

		// Add it to the "createReflected" method
		sw.println("if (objClassName.equals(\"" + jBeanClass + "\")) {");

		sw.println("  return new BeanProperty[] {");

		boolean first = true;

		// Describe the bean properties
		PropertyDescriptor[] properties = PropertyUtils
				.getPropertyDescriptors(beanClass);
		for (PropertyDescriptor p : properties) {

			Method readMethod = p.getReadMethod();

			String name = p.getName();
			if (name.equals("class")) { // doesn't use this inner
				// property
				continue;
			}

			if (readMethod == null) { // If the property cannot be read
				continue;
			}

			String type = validatePropertyTypeForCast(p.getPropertyType()
					.getName());
			if (type == null) {
				continue;
			}

			if (first) {
				first = false;
			} else {
				sw.println(",");
			}

			String accessType = (p.getWriteMethod() != null) ? "READ_WRITE"
					: "READ_ONLY";

			sw.print("    new BeanProperty(\"" + name + "\", \"" + type
					+ "\", BeanProperty.Access." + accessType + ")");
		}

		sw.println("  };");

		sw.println("}");
	}

	/**
	 * Generate the source code of the Reflected Wrapper
	 * 
	 * @param bean
	 * @param src
	 */
	private void generateGettersAndSettersCallers(JClassType bean,
			SourceWriter src, String beanClassName)
			throws UnableToCompleteException {

		// Find the bean class
		Class<?> beanClass = null;
		String jBeanClass = bean.getPackage().getName() + "." + bean.getName();
		try {
			beanClass = Class.forName(jBeanClass);
		} catch (Exception e) {
			logger.log(TreeLogger.ERROR, "Unable to find class  '" + jBeanClass
					+ "'", null);
			throw new UnableToCompleteException();
		}

		// Describe the bean properties
		PropertyDescriptor[] properties = PropertyUtils
				.getPropertyDescriptors(beanClass);

		// Generate getters
		src.println("protected Object callGetter(" + beanClassName
				+ " bean, String property) throws ReflectionException {");
		for (PropertyDescriptor p : properties) {

			Method readMethod = p.getReadMethod();

			String name = p.getName();
			if (name.equals("class")) // doesn't use this inner property
				continue;
			if (readMethod == null) // If the property cannot be read
				continue;

			// Call the getter on the bean
			src.println("if (property.equals(\"" + name + "\"))");
			src.println("return ((" + beanClassName + ")bean)."
					+ readMethod.getName() + "();");
		}
		src
				.println("throw new ReflectionException(\"The property cannot be get\");");
		src.println("}");

		// Generate setters
		src
				.println("protected void callSetter("
						+ beanClassName
						+ " bean, String property, Object value) throws ReflectionException {");
		for (PropertyDescriptor p : properties) {

			Method writeMethod = p.getWriteMethod();
			String name = p.getName();
			if (name.equals("class")) // doesn't use this inner property
				continue;
			if (writeMethod == null) // If the property cannot be write
				continue;

			// Call the setter on the bean
			src.println("if (property.equals(\"" + name + "\")) {");
			String propertyBasedClass = p.getWriteMethod().getDeclaringClass()
					.getName();
			src
					.println("(("
							+ propertyBasedClass
							+ ")bean)."
							+ writeMethod.getName()
							+ "(("
							+ validatePropertyTypeForCast(p.getPropertyType()
									.getName()) + ")value);");
			src.println("return;");
			src.println("}");

		}
		src
				.println("throw new ReflectionException(\"The property cannot be set\");");
		src.println("}");
	}

	/**
	 * Initialize the creation of the reflected class
	 */
	private SourceWriter initReflectedClass(JClassType requestedBeanClass,
			String reflectedName, String reflectedPackage,
			String reflectedClassFullName) {

		// Create the source writer and return it
		PrintWriter printWriter = context.tryCreate(logger, reflectedPackage,
				reflectedName);
		if (printWriter == null) {
			return null;
		}
		ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
				reflectedPackage, reflectedName);
		String superClassNameFull = Reflected.class.getName() + "<"
				+ reflectedClassFullName + ">";
		composerFactory.setSuperclass(superClassNameFull);
		composerFactory.addImport(FACTORY_PACKAGE + ".ReflectionException");
		return composerFactory.createSourceWriter(context, printWriter);
	}

	/**
	 * Finalize the creation of the reflected class
	 * 
	 * @param sourceWriter
	 */
	private void finalizeReflectedClass(SourceWriter sourceWriter) {
		sourceWriter.commit(logger);
	}

	private String validatePropertyTypeForCast(String value) {
		if (value.equals("boolean")) {
			return "Boolean";
		}
		if (value.equals("int")) {
			return "Integer";
		}
		if (value.equals("double")) {
			return "Double";
		}
		if (value.equals("float")) {
			return "Float";
		}
		if (value.equals("char")) {
			return "Character";
		}
		if (value.equals("byte")) {
			return "Byte";
		}
		if (value.equals("short")) {
			return "Short";
		}
		if (value.equals("long")) {
			return "Long";
		}
		return value;
	}
}
